Skip to content

Conversation

grdsdev
Copy link
Contributor

@grdsdev grdsdev commented Oct 6, 2025

Summary

This PR ports the getClaims feature from auth-js#1030 and follow-up improvements from auth-js#1078 and auth-js#1080 to supabase-swift. The getClaims method allows developers to verify and extract claims from JWTs with significant performance improvements through global caching and graceful key rotation handling.

What changed

New Types

  • JWK, JWKS: JSON Web Key and JSON Web Key Set types for asymmetric key verification
  • JWTHeader, JWTClaims: Strongly-typed JWT header and claims structures
  • JWTClaimsResponse: Response type containing verified claims, header, and signature
  • AudienceClaim: Enum to handle audience claim as either string or array
  • GetClaimsOptions: Options struct with allowExpired and custom jwks parameters
  • CachedJWKS: Internal struct tracking JWKS with timestamp
  • GlobalJWKSCache: Actor for thread-safe global JWKS caching

New Files

  • Base64URL.swift: Base64URL encoding/decoding utilities
  • JWTVerifier.swift: JWT signature verification using Apple Security
  • DecodedJWT: Extended JWT helper to decode complete JWT structure

Modified Files

  • AuthClient.swift:
    • Added getClaims(jwt:options:) method for JWT verification
    • Added global JWKS caching with TTL (10 minutes)
    • Added fetchJWK(kid:jwks:) private method with cache expiry checking
    • Implemented graceful fallback when JWK not found (key rotation support)
  • AuthError.swift: Added jwtVerificationFailed error case
  • Types.swift: Added all new JWT-related types and GetClaimsOptions
  • JWT.swift: Added decode(_:) method to return full JWT structure

How it works

The getClaims method provides flexible JWT verification with performance optimizations:

  1. Symmetric JWTs (HS256): Verified server-side via getUser() API call
  2. Asymmetric JWTs (RS256): Verified client-side via using Apple Security
  3. Global JWKS Cache: Shared across all clients with the same storage key, especially beneficial for serverless environments
  4. Graceful Key Rotation: Automatically falls back to server-side verification when JWK not found in cache

Usage Example

// Verify current session's JWT
let response = try await client.auth.getClaims()
print("User ID: \(response.claims.sub ?? "N/A")")
print("Email: \(response.claims.email ?? "N/A")")
print("Role: \(response.claims.role ?? "N/A")")

// Verify specific JWT
let customToken = "eyJhbGci..."
let response = try await client.auth.getClaims(jwt: customToken)

// Allow expired JWTs (useful for testing)
let response = try await client.auth.getClaims(
  options: GetClaimsOptions(allowExpired: true)
)

// Use custom JWKS
let customJWKS = JWKS(keys: [...])
let response = try await client.auth.getClaims(
  options: GetClaimsOptions(jwks: customJWKS)
)

Related

🤖 Generated with Claude Code

This commit adds JWT claims verification and extraction functionality
to the Auth client, porting the feature from auth-js PR #1030.

Key changes:
- Add Base64URL encoding/decoding utilities
- Extend JWT helper to decode full JWT (header, payload, signature)
- Add JWK types (JWK, JWKS, JWTHeader, JWTClaims, etc.)
- Add JWTVerifier for asymmetric JWT signature verification (ES256)
- Implement getClaims method in AuthClient
- Add jwtVerificationFailed error to AuthError

The getClaims method verifies JWT signatures and returns claims:
- For HS256 (symmetric) and RS256 JWTs: validates server-side via getUser
- For ES256 JWTs: verifies signature client-side using CryptoKit
- Supports custom JWKS or fetches from /.well-known/jwks.json
- Caches JWKS to minimize network requests

Note: RS256 client-side verification will be added once swift-crypto's
RSA API becomes public. Currently falls back to server-side verification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@grdsdev grdsdev marked this pull request as draft October 6, 2025 20:38
grdsdev and others added 2 commits October 6, 2025 17:48
This commit applies changes from auth-js PR #1078 to improve getClaims
performance and remove its experimental status.

Key changes:
- Add global JWKS cache shared across all clients with the same storage key
- Implement JWKS cache expiry with TTL (10 minutes)
- Add GetClaimsOptions struct with allowExpired and custom jwks options
- Remove experimental warning from getClaims documentation
- Update getClaims to accept options parameter instead of separate jwks param
- Add CachedJWKS struct to track cache timestamps
- Implement GlobalJWKSCache actor for thread-safe global caching

Performance improvements:
- Global cache significantly reduces JWKS fetches in serverless environments
- Cache TTL prevents stale keys while minimizing network requests
- Especially beneficial for AWS Lambda, Cloud Functions, etc.

Breaking change:
- getClaims now accepts GetClaimsOptions instead of JWKS parameter
- Old: getClaims(jwt:jwks:)
- New: getClaims(jwt:options:)

Migration:
```swift
// Before
let response = try await client.auth.getClaims(jwks: customJWKS)

// After
let response = try await client.auth.getClaims(
  options: GetClaimsOptions(jwks: customJWKS)
)

// With allowExpired
let response = try await client.auth.getClaims(
  options: GetClaimsOptions(allowExpired: true)
)
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
This commit applies changes from auth-js PR #1080 to handle key rotation
scenarios more gracefully.

Key changes:
- Change fetchJWK to return optional JWK? instead of throwing errors
- Return nil when JWKS is empty or key not found in JWKS
- Restructure getClaims logic to try fetching JWK first
- Fallback to server-side verification (getUser) if key not found
- Handle symmetric algorithms (HS256) and RS256 with nil check

Why this matters:
When developers rotate keys faster than cache TTL (10 minutes), a JWT
may be signed with a key ID that's not yet in the cached JWKS. Instead
of failing with an error, the method now gracefully falls back to
server-side verification via getUser().

This ensures:
- Zero downtime during key rotation
- Better resilience against cache staleness
- Transparent fallback for users

Example scenario:
1. JWKS is cached with key ID "abc123"
2. Admin rotates standby key to active (new key ID "xyz789")
3. User receives JWT signed with "xyz789"
4. fetchJWK returns nil (key not in cache)
5. getClaims automatically falls back to getUser()
6. Verification succeeds server-side

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@coveralls
Copy link

coveralls commented Oct 6, 2025

Pull Request Test Coverage Report for Build 18316862621

Details

  • 193 of 331 (58.31%) changed or added relevant lines in 7 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.8%) to 77.283%

Changes Missing Coverage Covered Lines Changed/Added Lines %
Sources/Helpers/JWT.swift 25 26 96.15%
Sources/Helpers/Base64URL.swift 12 18 66.67%
Sources/Auth/Internal/JWTAlgorithm.swift 0 13 0.0%
Sources/Auth/AuthClient.swift 104 127 81.89%
Sources/Auth/Types.swift 49 95 51.58%
Sources/Auth/Internal/JWK+RSA.swift 0 49 0.0%
Totals Coverage Status
Change from base Build 18289195894: -0.8%
Covered Lines: 5872
Relevant Lines: 7598

💛 - Coveralls

@grdsdev grdsdev marked this pull request as ready for review October 7, 2025 12:09
@grdsdev grdsdev requested a review from a team October 7, 2025 12:09
@grdsdev grdsdev merged commit fda262b into main Oct 7, 2025
22 checks passed
@grdsdev grdsdev deleted the feat/get-claims-method branch October 7, 2025 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants